package internal

import (
	"context"
	"errors"
	"go/ast"
	"go/token"
	"go/types"
	"strings"

	"golang.org/x/tools/go/packages"
)

// BindArgs describes the arguments passed to an injector function.
type BindArgs struct {
	// Name is the name of the injector function.
	Name string
	// Tuple represents the arguments.
	Tuple *types.Tuple
	// Pos is the source position of the injector function.
	Pos token.Pos
}

// LoadPackages typechecks the packages that match the given patterns and
// includes source for all transitive dependencies. The patterns are
// defined by the underlying build system. For the go tool, this is
// described at https://golang.org/cmd/go/#hdr-Package_lists_and_patterns
//
// wd is the working directory and env is the set of environment
// variables to use when loading the packages specified by patterns. If
// env is nil or empty, it is interpreted as an empty set of variables.
// In case of duplicate environment variables, the last one in the list
// takes precedence.
func LoadPackages(
	ctx context.Context,
	pwd string,
	env []string,
	tags string,
	patterns []string,
) ([]*packages.Package, []error) {
	cfg := &packages.Config{
		Context: ctx,
		Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
			packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax |
			packages.NeedTypesInfo | packages.NeedDeps,
		Dir:        pwd,
		Env:        env,
		BuildFlags: []string{"-tags=bindgen"},
	}

	if len(tags) > 0 {
		cfg.BuildFlags[0] += " " + tags
	}

	escaped := make([]string, len(patterns))
	for i := range patterns {
		escaped[i] = "pattern=" + patterns[i]
	}

	pkgs, err := packages.Load(cfg, escaped...)
	if err != nil {
		return nil, []error{err}
	}

	var errs []error

	for _, p := range pkgs {
		for _, e := range p.Errors {
			errs = append(errs, e)
		}
	}

	if len(errs) > 0 {
		return nil, errs
	}

	return pkgs, nil
}

// qualifiedIdentObject finds the object for an identifier or a
// qualified identifier, or nil if the object could not be found.
func qualifiedIdentObject(info *types.Info, expr ast.Expr) types.Object {
	switch expr := expr.(type) {
	case *ast.Ident:
		return info.ObjectOf(expr)
	case *ast.SelectorExpr:
		pkgName, isOk := expr.X.(*ast.Ident)
		if !isOk {
			return nil
		}

		if _, isOk := info.ObjectOf(pkgName).(*types.PkgName); !isOk {
			return nil
		}

		return info.ObjectOf(expr.Sel)
	default:
		return nil
	}
}

// funcOutput validates an injector or provider function's return signature.
func funcOutput(sig *types.Signature) error {
	if results := sig.Results(); results.Len() > 0 {
		return ErrHaveReturn
	}

	return nil
}

// findBuild returns the wire.Build call if fn is an injector template.
// It returns nil if the function is not an injector template.
func findBuild(info *types.Info, fun *ast.FuncDecl) (*ast.CallExpr, error) {
	if fun.Body == nil {
		return nil, ErrInvalidValue
	}

	numStatements := 0
	invalid := false

	var bindgenBuildCall *ast.CallExpr

	for _, stmt := range fun.Body.List {
		switch stmt := stmt.(type) {
		case *ast.ExprStmt:
			numStatements++
			if numStatements > 1 {
				invalid = true
			}

			call, isOk := stmt.X.(*ast.CallExpr)
			if !isOk {
				continue
			}

			if qualifiedIdentObject(info, call.Fun) == types.Universe.Lookup("panic") {
				if len(call.Args) != 1 {
					continue
				}

				call, isOk = call.Args[0].(*ast.CallExpr)
				if !isOk {
					continue
				}
			}

			buildObj := qualifiedIdentObject(info, call.Fun)
			if buildObj == nil || buildObj.Pkg() == nil || !isBindgenImport(buildObj.Pkg().Path()) ||
				buildObj.Name() != "Build" {
				continue
			}

			bindgenBuildCall = call
		case *ast.EmptyStmt:
		case *ast.ReturnStmt:
			if numStatements == 0 {
				return nil, ErrInvalidValue
			}
		default:
			invalid = true
		}
	}

	if bindgenBuildCall == nil {
		return nil, ErrInvalidValue
	}

	if invalid {
		// nolint
		return nil, errors.New("a call to wire.Build indicates that this function is an injector, but injectors must consist of only the wire.Build call and an optional return")
	}

	return bindgenBuildCall, nil
}

func isBindgenImport(path string) bool {
	const vendorPart = "vendor/"
	if i := strings.LastIndex(path, vendorPart); i != -1 && (i == 0 || path[i-1] == '/') {
		path = path[i+len(vendorPart):]
	}

	return path == "gitee.com/xuender/bindgen"
}
